<?php
/////////////////////////////////////////////////////////////////////////////
// 这个文件是 FleaPHP 项目的一部分
//
// Copyright (c) 2005 - 2006 FleaPHP.org (www.fleaphp.org)
//
// 要查看完整的版权信息和许可信息，请查看源代码中附带的 COPYRIGHT 文件，
// 或者访问 http://www.fleaphp.org/ 获得详细信息。
/////////////////////////////////////////////////////////////////////////////

/**
 * 定义 FLEA_Db_TableDataGateway 类
 *
 * @copyright Copyright (c) 2005 - 2006 FleaPHP.org (www.fleaphp.org)
 * @author 廖宇雷 dualface@gmail.com
 * @package Core
 * @version $Id: TableDataGateway.php 804 2007-04-08 08:29:46Z dualface $
 */

// {{{ constants
/**
 * HAS_ONE 关联表示一个记录拥有另一个关联的记录
 */
define('HAS_ONE',       1);

/**
 * BELONGS_TO 关联表示一个记录属于另一个记录
 */
define('BELONGS_TO',    2);

/**
 * HAS_MANY 关联表示一个记录拥有多个关联的记录
 */
define('HAS_MANY',      3);

/**
 * MANY_TO_MANY 关联表示两个数据表的数据互相引用
 */
define('MANY_TO_MANY',  4);
// }}}

/**
 * FLEA_Db_TableDataGateway 类（表数据入口）封装了数据表的 CRUD 操作
 *
 * 对于应用程序用到的每一个数据表，开发者都应该从 FLEA_Db_TableDataGateway 派生自己的类。
 * 并通过添加方法来封装针对该数据表的更复杂的数据库操作。
 *
 * 对于每一个表数据入口对象，都必须在类定义中通过 $tableName 和 $primaryKey
 * 来分别指定数据表的名字和主键字段名。
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.2
 */
class FLEA_Db_TableDataGateway
{
    /**
     * 数据表名（没有添加前缀）
     *
     * @var string
     */
    var $tableName = null;

    /**
     * 包含前缀的完整数据表名称
     *
     * @var string
     */
    var $fullTableName = null;

    /**
     * 主键字段名
     *
     * @var sring
     */
    var $primaryKey = null;

    /**
     * 定义一对一关联
     *
     * 1. 外键放置在关联表中；
     * 2. 保存时如果有关联对象，则自动创建或更新关联表的记录；
     * 3. 删除主表记录时自动删除关联记录。
     *
     * 示例：
     *
     * 1. 主表为 users，用于存储用户账户；
     * 2. 每个用户账户拥有一个且只有一个对应的个人信息（profile）记录；
     * 3. 个人信息存储在 profiles 表中，对应的表数据入口对象名为 Table_Profiles；
     * 4. 当删除一个用户账户时，该用户账户对应的个人信息记录也会被自动删除。
     *
     * <code>
     * class Table_Users extends FLEA_Db_TableDataGateway
     * {
     *     var $hasOne = array(
     *         array(
     *             'tableClass'  => 'Table_Profiles',
     *             'foreignKey'  => 'profile_id',
     *             'mappingName' => 'profile',
     *         ),
     *         array(
     *             ..... 另一个 hasOne 关联
     *         ),
     *     );
     * }
     * </code>
     *
     * 在上面的格式中，$hasOne 必须严格匹配大小写，同时必须是一个数组。
     * tableClass、foreignKey 等则是指定一个关联的特定属性。
     *
     * 1. foreignKey 属性指示在关联表中用什么字段存储对主表的主键引用；
     * 2. mappingName 属性指示在主表的记录中，以什么名字保存关联表的数据。
     *    以上面的例子而言，可以理解为主表的 profile 字段映射到了 profiles 表的一条记录；
     * 3. 如果不指定 foreignKey 属性，则假定使用关联表的主键字段名作为 foreignKey；
     * 4. 如果不指定 mappingName 属性，则假定使用 tableClass 作为 mappingName。
     *
     * @var array
     */
    var $hasOne = null;

    /**
     * 定义从属关联
     *
     * 1. 外键放置在主表中；
     * 2. 保存时不会自动更新关联表的记录；
     * 3. 删除时也不会更新关联表的记录。
     *
     * 示例：
     *
     * 1. 多个文章都属于某一个栏目；
     * 2. 删除一篇文章时，不会删除该文章所属的栏目。
     *
     * <code>
     * class Articles extends FLEA_Db_TableDataGateway
     * {
     *     var $belongsTo = array(
     *         array(
     *             'tableClass'  => 'Columns',
     *             'foreignKey'  => 'column_id',
     *             'mappingName' => 'column'
     *         ),
     *         array(
     *             ..... 另一个 belongsTo 关联
     *         ),
     *     );
     * }
     * </code>
     *
     * foreignKey 属性指示在当前表中用什么字段存储对关联表的主键引用。
     * 这恰恰和 HAS ONE 及 HAS MANY 关联相反。
     *
     * @var array
     */
    var $belongsTo = null;

    /**
     * 定义一对多关联
     *
     * 1. 外键保存在关联表中；
     * 2. 保存时自动更新关联表的记录；
     * 3. 删除主表记录时自动删除关联记录。
     *
     * 示例：
     *
     * 每个用户（user）有多张订单 order。
     *
     * <code>
     * class Users extends FLEA_Db_TableDataGateway
     * {
     *     var $hasMany = array(
     *         array(
     *             'tableClass'  => 'Orders',
     *             'foreignKey'  => 'user_id',
     *             'mappingName' => 'orders',
     *         ),
     *         array(
     *             ..... 另一个 hasMany 关联
     *         ),
     *     );
     * }
     * </code>
     *
     * 一对多本质上和一对一完全一样，唯一的不同在于 mappingName
     * 指示的主表字段对应的是关联表中的多条记录。
     * 所以可以将 HAS ONE 看作 HAS MANY 的一种特例。
     *
     * @var array
     */
    var $hasMany = null;

    /**
     * 定义多对多关联
     *
     * 1. 外键保存在中间表里面；
     * 2. 保存时自动更新中间表；
     * 3. 删除主表记录时自动删除中间表的相关记录。
     *
     * 示例：
     *
     * 1. 每个成员（member）可以拥有多个角色（role）；
     * 2. 而每个角色也可以指定给多个成员；
     * 3. 删除成员时，自动清除该成员和角色之间的关联关系。
     *
     * <code>
     * class Members extends FLEA_Db_TableDataGateway
     * {
     *     var $manyToMany = array(
     *         array(
     *             'tableClass'      => 'Roles',
     *             'joinTable'       => 'member_roles',
     *             'foreignKey'      => 'member_id',
     *             'assocforeignKey' => 'role_id',
     *             'mappingName'     => 'roles',
     *         ),
     *         array(
     *             ..... 另一个 manyToMany 关联
     *         ),
     *     );
     * }
     * </code>
     *
     * 在多对多关系中，当前表并不包含对关联表的主键引用。
     * 而是在一个中间表里面保存对当前表和关联表的主键引用。
     *
     * joinTable 属性指示用中间表的名字。foreignKey 属性和
     * assocforeignKey 属性分别指示在中间表里面用什么字段存储对主表和
     * 关联表主键字段的引用。
     *
     * 中间表通常只有两个字段，分别用来存储主表和关联表的主键值。
     *
     * @var array
     */
    var $manyToMany = null;

    /**
     * 当前数据表的元数据
     *
     * 元数据是一个二维数组，每一个元素的键名就是全大写的字段名，
     * 而键值则是该字段的数据表定义。
     *
     * @var array
     */
    var $meta = null;

    /**
     * 指示是否对数据进行自动验证
     *
     * 当 autoValidating 为 true 时，create() 和 update() 方法将对数据进行验证。
     *
     * @var boolean
     */
    var $autoValidating = false;

    /**
     * 用于数据验证的对象
     *
     * @var FLEA_Helper_Verifier
     */
    var $verifier = null;

    /**
     * 创建记录时，要自动填入当前时间的字段
     *
     * 只要数据表具有下列字段之一，则调用 create() 方法创建记录时，
     * 将以服务器时间自动填充该字段。
     *
     * @var array
     */
    var $createdTimeFields = array('CREATED', 'CREATED_ON', 'CREATED_AT');

    /**
     * 创建和更新记录时，要自动填入当前时间的字段
     *
     * 只要数据表具有下列字段之一，则调用 create() 方法创建记录或 update() 更新记录时，
     * 将以服务器时间自动填充该字段。
     *
     * @var array
     */
    var $updatedTimeFields = array('UPDATED', 'UPDATED_ON', 'UPDATED_AT');

    /**
     * 指示进行 CRUD 操作时是否处理关联
     *
     * 开发者应该使用 enableLinks() 和 disableLinks() 方法来启用或禁用关联处理。
     *
     * @var boolean
     */
    var $autoLink = true;

    /**
     * 数据库访问对象
     *
     * 开发者不应该直接访问该成员变量，而是通过 setDBO() 和 getDBO() 方法
     * 来访问表数据入口使用数据访问对象。
     *
     * @var FLEA_Db_Driver_Prototype
     */
    var $dbo = null;

    /**
     * 存储关联信息
     *
     * $links 是一个数组，数组中保存 TableLink 对象。
     * 开发者应该使用 getLink() 和 createLink()
     * 等方法来访问这些关联对象。
     *
     * @var array
     */
    var $links = array();

    /**
     * 包含前缀的数据表完全限定名
     *
     * @var string
     * @access private
     */
    var $qtableName;

    /**
     * 主键字段完全限定名
     *
     * @var string
     * @access private
     */
    var $qpk;

    /**
     * 用于关联查询时的主键字段别名
     */
    var $pka;

    /**
     * 用于关联查询时的主键字段完全限定名
     *
     * @var string
     * @access private
     */
    var $qpka;

    /**
     * 保存最后一次数据验证的结果
     *
     * 调用 getLastValidation() 方法可以获得最后一次数据验证的结果。
     *
     * @var array
     */
    var $lastValidationResult;

    /**
     * 构造 FLEA_Db_TableDataGateway 实例
     *
     * $params 参数允许有下列选项：
     * 1. tableName: 指定数据表的名称；
     * 2. autoValidating: 指示是否使用自动验证；
     * 3. verifierProvider: 指定要使用的数据验证服务对象。
     *    如果未指定。则使用应用程序设置 verifierProvider 指定的验证服务提供对象；
     * 4. skipConnect: 指示初始化表数据入口对象时是否不连接到数据库；
     * 5. dbDSN: 指定连接数据库要使用的 DSN，如果未指定则使用默认的 DSN 设置；
     * 6. dbo: 指定要使用的数据库访问对象;
     * 7. skipCreateLinks: 指示初始化表数据入口时，是否不建立关联关系
     *
     * @param array $params
     *
     * @return FLEA_Db_TableDataGateway
     */
    function FLEA_Db_TableDataGateway($params = null)
    {
        // 确定数据表名称
        if (isset($params['tableName'])) {
            $this->tableName = $params['tableName'];
        }

        // 初始化验证服务对象
        if (isset($params['autoValidating'])) {
            $this->autoValidating = $params['autoValidating'];
        }
        if ($this->autoValidating) {
            if (isset($params['verifierProvider'])) {
                $provider = $params['verifierProvider'];
            } else {
                $provider = FLEA::getAppInf('verifierProvider');
            }
            if ($provider != '') {
                $this->verifier =& FLEA::getSingleton($provider);
            }
        }

        // 当 skipInit 为 true 时，不初始化表数据入口对象
        if (isset($params['skipConnect']) && $params['skipConnect'] != false) {
            return;
        }

        // 初始化数据访问对象
        if (!isset($params['dbo'])) {
            if (isset($params['dbDSN'])) {
                $dbo =& FLEA::getDBO($params['dbDSN']);
            } else {
                $dbo =& FLEA::getDBO(FLEA::getAppInf('dbDSN'));
            }
        } else {
            $dbo =& $params['dbo'];
        }
        $this->setDBO($dbo);

        // 当 skipCreateLinks 不为 true 时，建立关联
        if (!isset($params['skipCreateLinks']) || $params['skipCreateLinks'] == false)
        {
            $this->relink();
        }
    }

    /**
     * 设置数据库访问对象
     *
     * @param FLEA_Db_Driver_Prototype $dbo
     *
     * @return boolean
     */
    function setDBO(& $dbo)
    {
        $this->dbo =& $dbo;

        $this->fullTableName = $dbo->dsn['dbTablePrefix'] . $this->tableName;
        $this->qtableName = $dbo->qtable($this->fullTableName);

        if (!$this->_prepareMeta()) {
            return false;
        }

        // 如果没有指定主键，则尝试自动获取
        if ($this->primaryKey == null) {
            foreach ($this->meta as $field) {
                if ($field['primaryKey']) {
                    $this->primaryKey = $field['name'];
                    break;
                }
            }
        }

        $this->qpk = $dbo->qfield($this->primaryKey, $this->fullTableName);
        $this->pka = 'flea_pkref_' . $this->primaryKey;
        $this->qpka = $this->qpk . ' AS ' . $this->pka;

        return true;
    }

    /**
     * 返回该表数据入口对象使用的数据访问对象
     *
     * @return FLEA_Db_Driver_Prototype
     */
    function & getDBO()
    {
        return $this->dbo;
    }

    /**
     * 返回符合条件的第一条记录及所有关联的数据，查询没有结果返回 false
     *
     * 用法：
     *
     * <code>
     * // $conditions 保存查询条件
     * $conditions = 'level_ix > 1';
     * // $tableOrders 是一个订单数据表的表数据入口对象
     * $order = $tableOrders->find($conditions, 'created DESC', 'id, title, body');
     *
     * $conditions = array('username' => 'dualface');
     * // $tableUsers 是一个用户信息数据表的表数据入口对象
     * $user = $tableUsers->find($conditions);
     * </code>
     *
     * $conditions 参数可以是整数、字符串和数组三种类型：
     *
     * - 如果 $conditions 参数是一个整数，则假定该整数为主键字段值
     *
     * - 如果 $conditions 参数是一个字符串，则该字符串将直接作为查询条件。
     *   <b>注意，当 $conditions 为字符串时，FleaPHP 不会对 $conditions 参数进行任何处理，
     *   因此务必需要小心，避免构造的查询条件包含 SQL 注入攻击。</b>
     *
     *   例如：
     *   <code>
     *   $conditions = 'name = ' . $table->qstr($_POST['name']);
     *   </code>
     *
     *   对于用户输入的查询条件，使用 FLEA_Db_TableDataGateway 的 qstr() 方法进行转义，
     *   确保消除 SQL 注入攻击。
     *
     * - 如果 $conditions 参数是一个数组，则数组的键名等同于字段名，
     *   而键值则等同于字段值。
     *
     *   <code>
     *   $conditions = array("name <> 'dualface'", "sex = 'male'");
     *   // 生成的查询条件将是 name <> 'dualface' AND sex = 'male'
     *   </code>
     *
     *   由于采用这种方式时，表数据入口会自动对字段值进行转义，
     *   因此不会存在 SQL 注入攻击的危险。
     *
     * - 如果 $conditions 参数是一个数组，但其中的元素没有键名，
     *   则假定键值为自定义查询条件：
     *
     *   <code>
     *   $conditions = array('LEFT(username, 5) = ' . $table->qstr($username));
     *   // 生成的查询条件将是 LEFT(username, 5) = 'dualface'
     *   </code>
     *
     * - $conditions 为数组时，可以混用两种风格：
     *
     *   <code>
     *   $conditions = array(
     *      'leve_ix > 3',
     *      'sex' => 'male',
     *      );
     *   // 生成的查询条件将是 level_ix > 3 AND sex = 'male'
     *
     * - $conditions 为数组时，多个查询条件之间将使用 AND 布尔运算符进行连接。
     *
     * $sort 参数指定查询时的排序方式，例如 'level_ix ASC' 表示按照 level_ix
     * 字段进行从小到大的排序。
     *
     * $fields 参数指定查询结果中要包含哪些字段，例如 'title, created' 表示只查询 title
     * 和 created 字段。当数据表的字段很多时，通过指定 $fields
     * 参数可以避免查询不需要的字段，从而提高性能。
     *
     * $fields 参数即可是以“,”逗号分隔的字段名，也可以是包含多个字段名的数组，例如：
     *
     * <code>
     * $fields = array('title', 'created');
     * </code>
     *
     * 推荐使用数组，这样表数据入口处理起来更快一些。
     *
     * @param mixed $conditions 查询条件
     * @param string $sort 排序
     * @param string|array $fields 指定要查询的字段
     *
     * @return array
     */
    function & find($conditions, $sort = null, $fields = '*')
    {
        $rowset =& $this->findAll($conditions, $sort, 1, $fields);
        if (is_array($rowset)) {
            $row = reset($rowset);
        } else {
            $row = false;
        }
        unset($rowset);
        return $row;
    }

    /**
     * 查询所有符合条件的记录及相关数据，返回一个包含多行记录的二维数组，
     * 失败时返回 false
     *
     * 用法：
     *
     * <code>
     * // $limit 是分页查询参数
     * $limit = array($length, $offset);
     * $conditions = array('status_value' => 4);
     * $sort = null;
     * $rowset = $table->findAll($conditions, $sort, $limit);
     * </code>
     *
     * 其他参数的用法与 find() 方法相同，增加的 $limit 参数可以是整数或一个数组。
     *
     * 当 $limit 参数为整数时，将返回查询结果中最开始的指定条数记录。
     * 返回的记录数由 $limit 确定。
     *
     * <code>
     * // 返回结果集中的最初 5 条记录
     * $limit = 5;
     * $rowset = $table->findAll(null, null, $limit);
     * </code>
     *
     * 如果 $limit 是一个数组，则假定这个数组由查询记录数和查询起始位置组成。
     *
     * <code>
     * // 返回结果集中从第 101 条记录开始的 20 条记录
     * $limit = array(20, 100);
     * $rowset = $table->findAll(null, null, $limit);
     * </code>
     *
     * <b>一定要注意，起始位置是从 0 开始计算的。因此起始位置 100 实际上表示返回
     * 从第 101 条开始的记录。</b>
     *
     * @param mixed $conditions 查询条件
     * @param string $sort 排序
     * @param mixed $limit 分页参数
     * @param string|array $fields 要查询的字段
     * @param boolean $queryLinks
     *
     * @return array
     */
    function & findAll($conditions = null, $sort = null, $limit = null, $fields = '*', $queryLinks = true)
    {
        list($whereby, $distinct) = $this->getWhere($conditions);
        // 处理排序
        $sortby = $sort != '' ? "\nORDER BY {$sort}" : '';
        // 处理 $limit
        if (is_array($limit)) {
            list($length, $offset) = $limit;
        } else {
            $length = $limit;
            $offset = null;
        }

        // 构造从主表查询数据的 SQL 语句
        $enableLinks = count($this->links) > 0 && $this->autoLink && $queryLinks;
        $fields = $this->dbo->qfields($fields, $this->fullTableName);
        if ($enableLinks) {
            // 当有关联需要处理时，必须获得主表的主键字段值
            $sql = "SELECT {$distinct}{$this->qpka}, {$fields}\nFROM {$this->qtableName}{$whereby}{$sortby}";
        } else {
            $sql = "SELECT {$distinct}{$fields}\nFROM {$this->qtableName}{$whereby}{$sortby}";
        }

        // 根据 $length 和 $offset 参数决定是否使用限定结果集的查询
        if (null !== $length || null !== $offset) {
            $result = $this->dbo->selectLimit($sql, $length, $offset);
        } else {
            $result = $this->dbo->execute($sql);
        }

        if ($enableLinks) {
            /**
             * 查询时同时将主键值单独提取出来，
             * 并且准备一个以主键值为键名的二维数组用于关联数据的装配
             */
            $pkvs = array();
            $assocRowset = null;
            $rowset = $this->dbo->getAllWithFieldRefs($result,
                    $this->pka, $pkvs, $assocRowset);
            $in = 'IN (';
            foreach ($pkvs as $pkv) {
                $in .= $this->dbo->qstr($pkv);
                $in .= ',';
            }
            $in = substr($in, 0, -1) . ')';
        } else {
            $rowset = $this->dbo->getAll($result);
        }
        unset($result);

        // 如果没有关联需要处理或者没有查询结果，则直接返回查询结果
        if (!$enableLinks || empty($rowset) || !$this->autoLink) {
            return $rowset;
        }

        /**
         * 遍历每一个关联对象，并从关联对象获取查询语句
         *
         * 查询获得数据后，将关联表的数据和主表数据装配在一起
         */
        $callback = create_function('& $row, $offset, $mappingName',
                '$row[$mappingName] = null;');
        foreach ($this->links as $link) {
            /* @var $link FLEA_Db_TableLink */
            if (!$link->enabled || !$link->linkRead) { continue; }
            array_walk($assocRowset, $callback, $link->mappingName);
            $sql = $link->getFindSQL($in);
            $this->dbo->assemble($sql, $assocRowset, $link->mappingName, $link->oneToOne, $this->pka);
        }

        return $rowset;
    }

    /**
     * 返回具有指定字段值的第一条记录
     *
     * 用法：
     * <code>
     * $row = $tableBooks->findByField('title' => 'The PHP5 Power ?');
     * </code>
     *
     * @param string $field 要查询的字段
     * @param mixed $value 要查询的字段值
     * @param string $sort 排序
     * @param string|array $fields 结果要包含的字段
     *
     * @return array
     */
    function & findByField($field, $value, $sort = null, $fields = '*')
    {
        return $this->find(array($field => $value), $sort, $fields);
    }

    /**
     * 返回具有指定字段值的所有记录
     *
     * 用法：
     * <code>
     * $row = $tableBooks->findByField('publishYear' => 2005);
     * </code>
     *
     * @param string $field 要查询的字段
     * @param mixed $value 要查询的字段值
     * @param string $sort 排序
     * @param array $limit 分页参数
     * @param string|array $fields 结果要包含的字段
     *
     * @return array
     */
    function & findAllByField($field, $value, $sort = null, $limit = null, $fields = '*')
    {
        return $this->findAll(array($field => $value), $sort, $limit, $fields);
    }

    /**
     * 直接使用 sql 语句获取记录（该方法不会处理关联数据表）
     *
     * 用法：
     * <code>
     * $rowset = $table->findBySql("SELECT * FROM mytable WHERE ix < 5 OR ix > 10");
     * </code>
     *
     * @param string $sql
     * @param mixed $limit
     *
     * @return array
     */
    function & findBySql($sql, $limit = null)
    {
        // 处理 $limit
        if (is_array($limit)) {
            list($length, $offset) = $limit;
        } else {
            $length = $limit;
            $offset = null;
        }
        if (is_null($length) && is_null($offset)) {
            return $this->dbo->getAll($sql);
        }

        $result = $this->dbo->selectLimit($sql, $length, $offset);
        if ($result) {
            return $this->dbo->getAll($result);
        }
        return false;
    }

    /**
     * 统计符合条件的记录的总数
     *
     * 用法：
     * <code>
     * $count = $table->findCount("ix > 6");
     * </code>
     *
     * @param mixed $conditions 统计条件
     * @param string|array $fields 统计记录时涉及的字段，默认为只统计主键字段
     *
     * @return int
     */
    function findCount($conditions = null, $fields = null)
    {
        list($whereby, $distinct) = $this->getWhere($conditions);
        if ($fields == null) {
            $fields = $this->qpk;
        } else {
            $fields = $this->dbo->qfields($fields, $this->fullTableName);
        }
        $sql = "SELECT {$distinct}COUNT({$fields})\nFROM {$this->qtableName}{$whereby}";
        return (int)$this->dbo->getOne($sql);
    }

    /**
     * 保存数据到数据库
     *
     * 如果数据包含主键值，则 save() 实际上会调用 update() 来更新记录。
     * 否则调用 create() 来创建记录。
     *
     * 用法：
     * <code>
     * // 该操作将插入一条新记录
     * $row = array(
     *     'title' => 'My New Book'
     * );
     * $tableBook->save($row);
     *
     * // 该操作将 book_id 为 5 的图书名称改为 'The PHP5 Power Second edition'
     * // 此处假定图书数据表的主键字段名为 book_id
     * $row = array(
     *     'book_id' => 5,
     *     'title' => 'The PHP5 Power Second edition'
     * );
     * $tableBook->save($row);
     * </code>
     *
     * @param array $row
     * @param boolean $saveLinks
     *
     * @return boolean
     */
    function save(& $row, $saveLinks = true)
    {
        if (!isset($row[$this->primaryKey]) || empty($row[$this->primaryKey])) {
            return $this->create($row, $saveLinks);
        } else {
            return $this->update($row, $saveLinks);
        }
    }

    /**
     * 替换一条现有记录或插入新记录，返回记录的主键值
     *
     * @param array $row
     *
     * @return mixed
     */
    function replace(& $row) {
        $this->_setCreatedTimeFields($row);
        $fields = '';
        $values = '';
        foreach ($row as $field => $value) {
            if (!isset($this->meta[strtoupper($field)])) { continue; }
            $fields .= $this->dbo->qfield($field) . ', ';
            $values .= $this->dbo->qstr($value) . ', ';
        }
        $fields = substr($fields, 0, -2);
        $values = substr($values, 0, -2);
        $sql = "REPLACE INTO {$this->fullTableName}\n    ({$fields})\nVALUES ({$values})";
        if (!$this->dbo->execute($sql)) { return null; }

        if (isset($row[$this->primaryKey]) && !empty($row[$this->primaryKey])) {
            return $row[$this->primaryKey];
        }

        return $this->dbo->insertId();
    }

    /**
     * 更新一条现有的记录，成功返回 true，失败返回 false
     *
     * 该操作会引发 _beforeUpdate()、_beforeUpdateDb() 和 _afterUpdateDb() 事件。
     *
     * 用法：
     * <code>
     * // 此处假定图书数据表的主键字段名为 book_id
     * // 该操作将 book_id 为 5 的图书名称改为 'The PHP5 Power Second edition'
     * $row = array(
     *     'book_id' => 5,
     *     'title' => 'The PHP5 Power Second edition'
     * );
     * $tableBook->update($row);
     * </code>
     *
     * @param array $row
     * @param boolean $saveLinks
     *
     * @return boolean
     */
    function update(& $row, $saveLinks = true)
    {
        if (!$this->_beforeUpdate($row)) {
            return false;
        }

        // 检查是否提供了主键值
        if (!isset($row[$this->primaryKey])) {
            FLEA::loadClass('FLEA_Db_Exception_MissingPrimaryKey');
            __THROW(new FLEA_Db_Exception_MissingPrimaryKey($this->primaryKey));
            return false;
        }

        // 自动填写记录的最后更新时间字段
        $this->_setUpdatedTimeFields($row);

        // 如果提供了验证器，则进行数据验证
        if ($this->autoValidating && $this->verifier != null) {
            if (!$this->checkRowData($row, true)) {
                // 验证失败抛出异常
                FLEA::loadClass('FLEA_Exception_ValidationFailed');
                __THROW(new FLEA_Exception_ValidationFailed($this->getLastValidation(),                        $row));
                return false;
            }
        }

        // 开始事务
        $this->dbo->startTrans();

        // 调用 _beforeUpdateDb() 事件
        if (!$this->_beforeUpdateDb($row)) {
            $this->dbo->completeTrans(false);
            return false;
        }

        // 生成 SQL 语句
        $pkv = $row[$this->primaryKey];
        unset($row[$this->primaryKey]);
        $fieldValuePairs = array();
        foreach ($row as $fieldName => $value) {
            if (!isset($this->meta[strtoupper($fieldName)])) { continue; }
            $fieldValuePairs[] = $this->dbo->qfield($fieldName, $this->fullTableName) .
                    ' = ' . $this->dbo->qstr($value);
        }
        $fieldValuePairs = implode(', ', $fieldValuePairs);

        $row[$this->primaryKey] = $pkv;
        $qpkv = $this->dbo->qstr($pkv);
        $where = "{$this->qpk} = {$qpkv}";
        if ($fieldValuePairs != '') {
            $sql = "UPDATE {$this->qtableName}\nSET {$fieldValuePairs}\nWHERE {$where}";
        } else {
            $sql = null;
        }

        // 执行更新操作
        if ($sql) {
            if (!$this->dbo->execute($sql)) {
                $this->dbo->completeTrans(false);
                return false;
            }
        }

        // 处理对关联数据的更新
        if ($this->autoLink && $saveLinks) {
            foreach ($this->links as $link) {
                /* @var $link FLEA_Db_TableLink */
                // 跳过不需要处理的关联
                if (!$link->enabled || !$link->linkUpdate
                    || !isset($row[$link->mappingName])
                    || !is_array($row[$link->mappingName]))
                {
                    continue;
                }

                if (!$link->saveAssocData($row[$link->mappingName], $pkv)) {
                    $this->dbo->completeTrans(false);
                    return false;
                }
            }
        }

        // 提交事务
        $this->dbo->completeTrans();

        $this->_afterUpdateDb($row);

        return true;
    }

    /**
     * 更新符合条件的记录，成功返回更新的记录总数，失败返回 false
     *
     * 用法：
     * <code>
     * // 将所有 ix 大于 5 的记录的 ix 字段值更新为 10，而 status_value 字段值更新为 3
     * $row = array(
     *     'ix' => 10,
     *     'status_value' => 3,
     * );
     * $table->updateByConditions('ix > 5', $data);
     *
     * // 将所有 status_value 等于 4 的记录的 ix 字段增加 1
     * $data = array(
     *     'ix = ix + 1',
     * );
     * $table->updateByConditions('status_value = 4', $$row);
     * </code>
     *
     * @param mixed $conditions
     * @param array $$row
     *
     * @return int|boolean
     */
    function updateByConditions($conditions, & $row)
    {
        $whereby = $this->getWhere($conditions, false);
        $this->_setUpdatedTimeFields($row);
        $fieldValuePairs = array();
        foreach ($row as $field => $value) {
            if (is_int($field)) {
                $fieldValuePairs[] = $value;
            } else {
                $fieldValuePairs[] = $this->dbo->qfield($field, $this->fullTableName) .
                        ' = ' . $this->dbo->qstr($value);
            }
        }
        $fieldValuePairs = implode(', ', $fieldValuePairs);
        $sql = "UPDATE {$this->qtableName}\nSET {$fieldValuePairs}{$whereby}";
        $this->dbo->startTrans();
        $this->dbo->execute($sql);
        $this->dbo->completeTrans();
        return true;
    }

    /**
     * 更新记录的指定字段，返回更新的记录总数
     *
     * 用法：
     * <code>
     * // 更新所有 status_value 等于 6 的记录的 ix 字段值为 15
     * $table->updateField('status_value = 6', 'ix', 15);
     * </code>
     *
     * @param mixed $conditions
     * @param string $field
     * @param mixed $value
     *
     * @return int
     */
    function updateField($conditions, $field, $value)
    {
        $row = array($field => $value);
        return $this->updateByConditions($conditions, $row);
    }

    /**
     * 增加符合条件的记录的指定字段的值，返回更新的记录总数
     *
     * 用法：
     * <code>
     * // 将所有 status_value 等于 7 的记录的 ix 字段值增加 3
     * $table->incrField('status_value = 7', 'ix', 3);
     * </code>
     *
     * @param mixed $conditions
     * @param string $field
     * @param int $incr
     *
     * @return mixed
     */
    function incrField($conditions, $field, $incr = 1)
    {
        $field = $this->dbo->qfield($field, $this->fullTableName);
        $incr = (int)$incr;
        $row = array("{$field} = {$field} + {$incr}");
        return $this->updateByConditions($conditions, $row);
    }

    /**
     * 减小符合条件的记录的指定字段的值，返回更新的记录总数
     *
     * 用法：
     * <code>
     * // 将所有 status_value 等于 8 的记录的 ix 字段值减小 2
     * $table->decrField('status_value = 8', 'ix', 2);
     * </code>
     *
     * @param mixed $conditions
     * @param string $field
     * @param int $decr
     *
     * @return mixed
     */
    function decrField($conditions, $field, $decr = 1)
    {
        $field = $this->dbo->qfield($field, $this->fullTableName);
        $decr = (int)$decr;
        $row = array("{$field} = {$field} - {$decr}");
        return $this->updateByConditions($conditions, $row);
    }

    /**
     * 插入一条新记录，返回新记录的主键值
     *
     * create() 操作会引发 _beforeCreate()、_beforeCreateDb() 和 _afterCreateDb() 事件。
     *
     * 用法：
     * <code>
     * $row = array('title' => 'My Book');
     * $tableBooks->create($row);
     * </code>
     *
     * @param array $row
     * @param boolean $saveLinks
     *
     * @return mixed
     */
    function create(& $row, $saveLinks = true)
    {
        if (!$this->_beforeCreate($row)) {
            return false;
        }

        // 自动设置日期字段
        $this->_setCreatedTimeFields($row);

        // 处理主键
        $mpk = strtoupper($this->primaryKey);
        $insertId = null;
        if (isset($this->meta[$mpk]['autoIncrement'])
            && $this->meta[$mpk]['autoIncrement'])
        {
            if (isset($row[$this->primaryKey])) {
                if (empty($row[$this->primaryKey])) {
                    // 如果主键字段是自增，而提供的记录数据虽然包含主键字段，
                    // 但却是空值，则删除这个空值
                    unset($row[$this->primaryKey]);
                } else {
                    // 如果主键字段是自增，而提供的记录数据又包含了主键字段，则抛出异常
                    /**
                    FLEA::loadClass('FLEA_Db_Exception_PrimaryKeyExists');
                    __THROW(new FLEA_Db_Exception_PrimaryKeyExists($this->primaryKey,
                            $row[$this->primaryKey]));
                    return false;
                    */
                }
            }
        } else {
            // 如果主键字段不是自增字段，并且没有提供主键字段值时，则获取一个新的主键字段值
            if (!isset($row[$this->primaryKey]) || empty($row[$this->primaryKey])) {
                $insertId = $this->newInsertId();
                $row[$this->primaryKey] = $insertId;
            } else {
                // 使用开发者提交的主键字段值
                $insertId = $row[$this->primaryKey];
            }
        }

        // 自动验证数据
        if ($this->autoValidating && $this->verifier != null) {
            if (!$this->checkRowData($row)) {
                FLEA::loadClass('FLEA_Exception_ValidationFailed');
                __THROW(new FLEA_Exception_ValidationFailed($this->getLastValidation(),
                        $row));
                return false;
            }
        }

        // 调用 _beforeCreateDb() 事件
        $this->dbo->startTrans();

        if (!$this->_beforeCreateDb($row)) {
            $this->dbo->completeTrans(false);
            return false;
        }

        // 生成 SQL 语句
        $fields = array();
        $values = array();
        foreach ($row as $field => $value) {
            $mfield = strtoupper($field);
            if (!isset($this->meta[$mfield])) { continue; }

            $fields[] = $field;
            $values[] = $this->dbo->qstr($value);
        }
        if (empty($fields)) { return false; }

        $fields = $this->dbo->qfields($fields);
        $values = implode(', ', $values);
        $sql = "INSERT INTO {$this->qtableName}\n    ({$fields})\nVALUES ({$values})";

        // 插入数据
        if (!$this->dbo->Execute($sql)) {
            $this->dbo->completeTrans(false);
            return false;
        }

        // 如果提交的数据中没有主键字段值，则尝试获取新插入记录的主键值
        if ($insertId == null) {
            $insertId = $this->dbo->insertId();
            if (!$insertId) {
                $this->dbo->completeTrans(false);
                FLEA::loadClass('FLEA_Db_Exception_InvalidInsertID');
                __THROW(new FLEA_Db_Exception_InvalidInsertID());
                return false;
            }
        }

        // 处理关联数据表
        if ($this->autoLink && $saveLinks) {
            foreach ($this->links as $link) {
                /* @var $link FLEA_Db_TableLink */
                if (!$link->enabled
                    || !$link->linkCreate
                    || !isset($row[$link->mappingName])
                    || !is_array($row[$link->mappingName]))
                {
                    // 跳过没有关联数据的关联和不需要处理的关联
                    continue;
                }

                if (!$link->saveAssocData($row[$link->mappingName], $insertId)) {
                    $this->dbo->completeTrans(false);
                    return false;
                }
            }
        }

        // 提交事务
        $this->dbo->CompleteTrans();

        $this->_afterCreateDb($row);

        return $insertId;
    }

    /**
     * 删除记录
     *
     * remove() 操作引发下列事件：
     * 1. _beforeRemove()
     * 2. _beforeRemoveDbByPkv()
     * 3. _afterRemoveDbByPkv()
     * 4. _afterRemoveDb()
     *
     * @param array $row
     *
     * @return boolean
     */
    function remove(& $row)
    {
        if (!$this->_beforeRemove($row)) {
            return false;
        }

        if (!isset($row[$this->primaryKey])) {
            FLEA::loadClass('FLEA_Db_Exception_MissingPrimaryKey');
            __THROW(new FLEA_Db_Exception_MissingPrimaryKey($this->primaryKey));
            return false;
        }
        $ret = $this->removeByPkv($row[$this->primaryKey]);
        if ($ret) {
            $this->_afterRemoveDb($row);
        }

        return $ret;
    }

    /**
     * 根据主键值删除记录
     *
     * removeByPkv() 引发 _beforeRemoveDbByPkv() 和 _afterRemoveDbByPkv() 事件。
     *
     * @param mixed $pkv
     *
     * @return boolean
     */
    function removeByPkv($pkv)
    {
        $this->dbo->startTrans();

        if (!$this->_beforeRemoveDbByPkv($pkv)) {
            $this->dbo->completeTrans(false);
            return false;
        }

        $qpkv = $this->dbo->qstr($pkv);
        $sql = "DELETE FROM {$this->qtableName}\nWHERE {$this->qpk} = {$qpkv}";
        if ($this->dbo->execute($sql) == false) {
            $this->dbo->completeTrans(false);
            return false;
        }

        // 处理关联数据表
        if ($this->autoLink) {
            foreach ($this->links as $link) {
                /* @var $link FLEA_Db_TableLink */
                switch ($link->type) {
                case MANY_TO_MANY:
                    /* @var $link FLEA_Db_ManyToManyLink */
                    if (!$link->deleteMiddleTableDataByMainForeignKey($qpkv)) {
                        $this->dbo->completeTrans(false);
                        return false;
                    }
                    break;
                case HAS_ONE:
                case HAS_MANY:
                    /**
                     * 对于 HAS_ONE 和 HAS_MANY 关联，分为两种情况处理
                     *
                     * 当 $link->linkRemove 为 true 时，直接删除关联表中的关联数据
                     * 否则更新关联数据的外键值为 $link->linkRemoveFillValue
                     */
                    /* @var $link FLEA_Db_HasOneLink */
                    if (!$link->deleteByForeignKey($qpkv)) {
                        $this->dbo->completeTrans(false);
                        return false;
                    }
                    break;
                }
            }
        }

        // 提交事务
        $this->dbo->completeTrans();

        $this->_afterRemoveDbByPkv($pkv);

        return true;
    }

    /**
     * 删除符合条件的记录
     *
     * @param mixed $conditions
     *
     * @return boolean
     */
    function removeByConditions($conditions)
    {
        $rowset = $this->findAll($conditions, null, null, $this->primaryKey, false);
        $ret = true;
        $this->dbo->startTrans();
        foreach ($rowset as $row) {
            $ret = $this->removeByPkv($row[$this->primaryKey]);
            if (!$ret) { break; }
        }
        $this->dbo->completeTrans();
        return $ret;
    }

    /**
     * 删除数组中所有主键值的记录，该操作不会处理关联
     *
     * @param array $pkvs
     *
     * @return boolean
     */
    function removeByPkvs($pkvs)
    {
        $ret = true;
        $this->dbo->startTrans();
        foreach ($pkvs as $id) {
            $ret = $this->removeByPkv($id);
            if (!$ret) { break; }
        }
        $this->dbo->completeTrans();
        return $ret;
    }

    /**
     * 删除所有记录
     *
     * @return boolean
     */
    function removeAll()
    {
        $sql = "DELETE FROM {$this->qtableName}";
        return $this->execute($sql);
    }

    /**
     * 删除所有记录及关联的数据
     *
     * @return boolean
     */
    function removeAllWithLinks()
    {
        $this->dbo->startTrans();

        $sql = "DELETE FROM {$this->qtableName}";
        if ($this->dbo->execute($sql) == false) {
            $this->dbo->completeTrans(false);
            return false;
        }

        // 处理关联数据表
        if ($this->autoLink) {
            foreach ($this->links as $link) {
                /* @var $link FLEA_Db_TableLink */
                switch ($link->type) {
                case MANY_TO_MANY:
                    /* @var $link FLEA_Db_ManyToManyLink */
                    $link->init();
                    $sql = "DELETE FROM {$link->joinTable}";
                    break;
                case HAS_ONE:
                case HAS_MANY:
                    $link->init();
                    $sql = "DELETE FROM {$link->assocTDG->fullTableName}";
                    break;
                default:
                    continue;
                }
                if ($this->dbo->execute($sql) == false) {
                    $this->dbo->completeTrans(false);
                    return false;
                }
            }
        }

        // 提交事务
        $this->dbo->completeTrans();

        return true;
    }

    /**
     * 启用所有关联
     */
    function enableLinks()
    {
        $this->autoLink = true;
        $keys = array_keys($this->links);
        foreach ($keys as $key) {
            $this->links[$key]->enabled = true;
        }
    }

    /**
     * 启用指定关联
     *
     * @param string $linkName
     *
     * @return FLEA_Db_TableLink
     *
     */
    function enableLink($linkName)
    {
        $link =& $this->getLink($linkName);
        if ($link) { $link->enabled = true; }
        $this->autoLink = true;
        return $link;
    }

    /**
     * 禁用所有关联
     */
    function disableLinks()
    {
        $this->autoLink = false;
        $keys = array_keys($this->links);
        foreach ($keys as $key) {
            $this->links[$key]->enabled = false;
        }
    }

    /**
     * 禁用指定关联
     *
     * @param string $linkName
     *
     * @return FLEA_Db_TableLink
     */
    function disableLink($linkName)
    {
        $link =& $this->getLink($linkName);
        if ($link) { $link->enabled = false; }
        return $link;
    }

    /**
     * 清除所有关联
     */
    function clearLinks()
    {
        $this->links = array();
    }

    /**
     * 根据类定义的 $hasOne、$hasMany、$belongsTo 和 $manyToMany 成员变量重建所有关联
     */
    function relink()
    {
        $this->clearLinks();
        $this->createLink($this->hasOne,     HAS_ONE);
        $this->createLink($this->belongsTo,  BELONGS_TO);
        $this->createLink($this->hasMany,    HAS_MANY);
        $this->createLink($this->manyToMany, MANY_TO_MANY);
    }

    /**
     * 获取指定名字的关联
     *
     * @param string $linkName
     *
     * @return FLEA_Db_TableLink
     */
    function & getLink($linkName)
    {
        $linkName = strtoupper($linkName);
        if (isset($this->links[$linkName])) {
            return $this->links[$linkName];
        }

        FLEA::loadClass('FLEA_Db_Exception_MissingLink');
        __THROW(new FLEA_Db_Exception_MissingLink($linkName));
        $ret = false;
        return $ret;
    }

    /**
     * 检查指定名字的关联是否存在
     *
     * @param string $name
     *
     * @return boolean
     */
    function & existsLink($name)
    {
        $name = strtoupper($name);
        return isset($this->links[$name]);
    }

    /**
     * 建立关联，并且返回新建立的关联对象
     *
     * @param array $defines
     * @param enum $type
     *
     * @return FLEA_Db_TableLink
     */
    function createLink($defines, $type)
    {
        if (!is_array($defines)) { return; }
        if (!is_array(reset($defines))) {
            $defines = array($defines);
        }

        // 创建关联对象
        foreach ($defines as $define) {
            if (!is_array($define)) { continue; }
            // 构造连接对象实例
            $link =& FLEA_Db_TableLink::createLink($define, $type, $this);
            $this->links[strtoupper($link->name)] =& $link;
        }
    }

    /**
     * 对数据进行验证
     *
     * 派生类可以覆盖此方法，以便进行附加的验证。
     *
     * @param array $row
     * @param boolean $skipEmpty
     *
     * @return boolean
     */
    function checkRowData(& $row, $skipEmpty = false) {
        if (is_null($this->verifier)) { return false; }
        $this->lastValidationResult =
                $this->verifier->checkAll($row, $this->meta, $skipEmpty);
        return empty($this->lastValidationResult);
    }

    /**
     * 返回最后一次数据验证的结果
     *
     * @return mixed
     */
    function getLastValidation() {
        return $this->lastValidationResult;
    }

    /**
     * 返回当前数据表的下一个插入 ID
     *
     * @return mixed
     */
    function newInsertId() {
        return $this->dbo->nextId($this->fullTableName . '_seq');
    }

    /**
     * 直接执行一个 sql 语句
     *
     * @param string $sql
     * @param array $inputarr
     *
     * @return mixed
     */
    function execute($sql, $inputarr = false)
    {
        return $this->dbo->execute($sql, $inputarr);
    }

    /**
     * 返回转义后的数据
     *
     * @param mixed $value
     *
     * @return string
     */
    function qstr($value)
    {
        return $this->dbo->qstr($value);
    }

    /**
     * 获得一个字段名的完全限定名
     *
     * @param string $fieldName
     * @param string $tableName
     *
     * @return string
     */
    function qfield($fieldName, $tableName = null)
    {
        if ($tableName == null) {
            $tableName = $this->fullTableName;
        }
        return $this->dbo->qfield($fieldName, $tableName);
    }

    /**
     * 获得多个字段名的完全限定名
     *
     * @param string|array $fieldsName
     * @param string $tableName
     *
     * @return string
     */
    function qfields($fieldsName, $tableName = null)
    {
        if ($tableName == null) {
            $tableName = $this->fullTableName;
        }
        return $this->dbo->qfields($fieldsName, $tableName);
    }

    /**
     * 分析查询条件，返回 WHERE 子句
     *
     * @param array $conditions
     * @param boolean $queryLinks
     *
     * @return string
     */
    function getWhere($conditions, $queryLinks = true) {
        // 处理查询条件
        $where = FLEA_Db_SqlHelper::parseConditions($conditions, $this);
        $sqljoin = '';
        $distinct = '';

        do {
            if (!is_array($where)) {
                $whereby = $where != '' ? "\nWHERE {$where}" : '';
                break;
            }

            $arr = $where;
            list($where, $linksWhere) = $arr;
            unset($arr);

            if (!$this->autoLink || !$queryLinks) {
                $whereby = $where != '' ? "\nWHERE {$where}" : '';
                break;
            }

            foreach ($linksWhere as $linkid => $lws) {
                if (!isset($this->links[$linkid]) || !$this->links[$linkid]->enabled) {
                    continue;
                }

                $link =& $this->links[$linkid];
                /* @var $link FLEA_Db_TableLink */
                if (!$link->init) { $link->init(); }
                $distinct = 'DISTINCT ';

                switch ($link->type) {
                case HAS_ONE:
                case HAS_MANY:
                    /* @var $link FLEA_Db_HasOneLink */
                    $sqljoin .= <<<EOT
LEFT JOIN {$link->assocTDG->qtableName}
ON {$link->mainTDG->qpk} = {$link->qforeignKey}
EOT;
                    break;
                case BELONGS_TO:
                    /* @var $link FLEA_Db_BelongsToLink */
                    $sqljoin .= <<<EOT
LEFT JOIN {$link->assocTDG->qtableName}
ON {$link->assocTDG->qpk} = {$link->qforeignKey}
EOT;
                    break;
                case MANY_TO_MANY:
                    /* @var $link FLEA_Db_ManyToManyLink */
                    $sqljoin .= <<<EOT
INNER JOIN {$link->qjoinTable}
ON {$link->qforeignKey} = {$this->qpk}
INNER JOIN {$link->assocTDG->qtableName}
ON {$link->assocTDG->qpk} = {$link->qassocForeignKey}
EOT;
                    break;
                }

                $whereby = $where != '' ? "WHERE {$where}" : 'WHERE';
                foreach ($lws as $lw) {
                    list($field, $value, $op, $expr, $isCommand) = $lw;
                    if ($whereby != 'WHERE') {
                        $whereby .= $expr;
                    }
                    if (!$isCommand) {
                        $field = $link->assocTDG->qfield($field);
                        $value = $this->dbo->qstr($value);
                        $whereby .= " {$field} {$op} {$value} ";
                    } else {
                        $whereby .= " {$value} ";
                    }
                }

                unset($link);
            }

            $whereby = "\n{$sqljoin}\n{$whereby}";
        } while (false);

        if ($queryLinks) {
            return array($whereby, $distinct);
        } else {
            return $whereby;
        }
    }

    /**
     * 强制刷新缓存的数据表 meta 信息
     */
    function flushMeta()
    {
        $this->_prepareMeta(true);
    }

    /**
     * 更新记录的 updated 等字段
     *
     * @param array $row
     */
    function _setUpdatedTimeFields(& $row) {
        foreach ($this->updatedTimeFields as $af) {
            $af = strtoupper($af);
            if (!isset($this->meta[$af])) { continue; }
            switch ($this->meta[$af]['simpleType']) {
            case 'D': // 日期
            case 'T': // 日期时间
                // 由数据库驱动获取时间格式
                $row[$this->meta[$af]['name']] = $this->dbo->dbTimeStamp(time());
                break;
            case 'I': // Unix 时间戳
                $row[$this->meta[$af]['name']] = time();
                break;
            }
        }
    }

    /**
     * 更新记录的 created 和 updated 等字段
     *
     * @param array $row
     */
    function _setCreatedTimeFields(& $row) {
        $currentTime = time();
        $currentTimeStamp = $this->dbo->dbTimeStamp(time());
        foreach (array_merge($this->createdTimeFields, $this->updatedTimeFields) as $af) {
            $af = strtoupper($af);
            if (!isset($this->meta[$af])) { continue; }
            $afn = $this->meta[$af]['name'];
            if (!empty($row[$afn])) { continue; }

            switch ($this->meta[$af]['simpleType']) {
            case 'D': // 日期
            case 'T': // 日期时间
                // 由数据库驱动获取时间格式
                $row[$afn] = $currentTimeStamp;
                break;
            case 'I': // Unix 时间戳
                $row[$afn] = $currentTime;
                break;
            }
        }
    }

    /**
     * 准备当前数据表的元数据
     *
     * @param boolean $flushCache
     *
     * @return boolean
     */
    function _prepareMeta($flushCache = false) {
        $cached = FLEA::getAppInf('dbMetaCached');
        if ($cached && !$flushCache) {
            $metaID = strtoupper($this->fullTableName);
            $cachedAllMeta = FLEA::getAppInf('FleaPHP.Cache.AllMeta');
            if (isset($cachedAllMeta[$metaID])) {
                $this->meta = $cachedAllMeta[$metaID];
                return true;
            }

            $cacheId = $this->dbo->dsn['id'];
            $allMeta = FLEA::getCache($cacheId, FLEA::getAppInf('dbMetaLifetime'));
            if (is_array($allMeta)) {
                FLEA::setAppInf('FleaPHP.Cache.AllMeta', $allMeta);
            }
            if (isset($allMeta[$metaID])) {
                $this->meta = $allMeta[$metaID];
                return true;
            }
        }

        $this->meta = $this->dbo->metaColumns($this->fullTableName);
        if ($this->meta == false) {
            FLEA::loadClass('FLEA_Db_Exception_MetaColumnsFailed');
            __THROW(new FLEA_Db_Exception_MetaColumnsFailed($this->tableName));
            return false;
        }
        if ($cached) {
            $allMeta[$metaID] = $this->meta;
            return FLEA::writeCache($cacheId, $allMeta);
        } else {
            return true;
        }
    }

    /**
     * 调用 create() 方法后立即引发 _beforeCreate 事件
     *
     * 如果要阻止 create() 创建记录，该方法应该返回 false，否则返回 true。
     *
     * @param array $row
     *
     * @return boolean
     */
    function _beforeCreate(& $row)
    {
        return true;
    }

    /**
     * 调用 create() 方法后，表数据入口对数据进行处理后存入数据库前引发 _beforeCreateDb 事件
     *
     * 如果要阻止 create() 创建记录，该方法应该返回 false，否则返回 true。
     *
     * @param array $row
     *
     * @return boolean
     */
    function _beforeCreateDb(& $row)
    {
        return true;
    }

    /**
     * 调用 create() 方法并且成功将数据存入数据库后引发 _afterCreateDb 事件
     *
     * @param array $row
     */
    function _afterCreateDb(& $row)
    {
    }


    /**
     * 调用 update() 方法后立即引发 _beforeUpdate 事件
     *
     * 如果要阻止 update() 更新记录，该方法应该返回 false，否则返回 true。
     *
     * @param array $row
     *
     * @return boolean
     */
    function _beforeUpdate(& $row)
    {
        return true;
    }

    /**
     * 调用 update() 方法后，表数据入口对数据进行处理后存入数据库前引发 _beforeUpdateDb 事件
     *
     * 如果要阻止 update() 更新记录，该方法应该返回 false，否则返回 true。
     *
     * @param array $row
     *
     * @return boolean
     */
    function _beforeUpdateDb(& $row)
    {
        return true;
    }

    /**
     * 调用 update() 方法并且成功将数据更新到数据库后引发 _afterUpdateDb 事件
     *
     * @param array $row
     */
    function _afterUpdateDb(& $row)
    {
    }

    /**
     * 调用 remove() 方法后立即引发 _beforeRemove 事件
     *
     * 如果要阻止 remove() 删除记录，该方法应该返回 false，否则返回 true。
     *
     * @param array $row
     *
     * @return boolean
     */
    function _beforeRemove(& $row)
    {
        return true;
    }

    /**
     * 调用 remove() 方法并且成功删除记录后引发 _afterRemoveDb 事件
     *
     * @param array $row
     */
    function _afterRemoveDb($row)
    {
    }

    /**
     * 调用 remove() 或 removeByPkv() 方法后立即引发 _beforeRemoveDbByPkv 事件
     *
     * 调用 remove() 方法时，_beforeRemoveDbByPkv 事件出现在 _beforeRemove 事件之后。
     *
     * 如果要阻止 remove() 或 removeByPkv() 删除记录，
     * 该方法应该返回 false，否则返回 true。
     *
     * @param mixed $pkv
     *
     * @return boolean
     */
    function _beforeRemoveDbByPkv($pkv)
    {
        return true;
    }

    /**
     * 调用 remove() 或 removeByPkv() 方法并且成功删除记录后引发 _afterRemoveDbByPkv 事件
     *
     * @param array $row
     */
    function _afterRemoveDbByPkv($pkv)
    {
    }
}


/**
 * FLEA_Db_TableLink 封装数据表之间的关联关系
 *
 * FLEA_Db_TableLink 是一个完全供 FleaPHP 内部使用的类，
 * 开发者不应该直接构造 FLEA_Db_TableLink 对象。
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.1
 */
class FLEA_Db_TableLink
{
    /**
     * 该连接的名字，用于检索指定的连接
     *
     * 同一个数据表的多个关联不能使用相同的名字。如果定义关联时没有指定名字，
     * 则以关联对象的 $mappingName 属性作为这个关联的名字。
     *
     * @var string
     */
    var $name;

    /**
     * 该关联所使用的表数据入口对象名
     *
     * @var string
     */
    var $tableClass;

    /**
     * 外键字段名
     *
     * @var string
     */
    var $foreignKey;

    /**
     * 关联数据表结果映射到主表结果中的字段名
     *
     * @var string
     */
    var $mappingName;

    /**
     * 在 TDG 将该关联的数据映射到对象时，recordObjectClass 属性指定对象名
     *
     * @var string
     */
    var $recordObjectClass;

    /**
     * 指示连接两个数据集的行时，是一对一连接还是一对多连接
     *
     * @var boolean
     */
    var $oneToOne;

    /**
     * 关联的类型
     *
     * @var enum
     */
    var $type;

    /**
     * 对关联表进行查询时使用的排序参数
     *
     * @var string
     */
    var $sort;

    /**
     * 对关联表进行查询时使用的条件参数
     *
     * @var string
     */
    var $conditions;

    /**
     * 对关联表进行查询时要获取的关联表字段
     *
     * @var string|array
     */
    var $fields = '*';

    /**
     * 当 enabled 为 false 时，表数据入口的任何操作都不会处理该关联
     *
     * enabled 的优先级高于 linkRead、linkCreate、linkUpdate 和 linkRemove。
     *
     * @var boolean
     */
    var $enabled = true;

    /**
     * 指示是否在主表读取记录时也读取该关联对应的关联表的记录
     *
     * @var boolean
     */
    var $linkRead = true;

    /**
     * 指示是否在主表创建记录时也创建该关联对应的关联表的记录
     *
     * @var boolean
     */
    var $linkCreate = true;

    /**
     * 指示是否在主表更新记录时也更新该关联对应的关联表的记录
     *
     * @var boolean
     */
    var $linkUpdate = true;

    /**
     * 指示是否在主表删除记录时也删除该关联对应的关联表的记录
     *
     * @var boolean
     */
    var $linkRemove = true;

    /**
     * 当删除主表记录而不删除关联表记录时，用什么值填充关联表记录的外键字段
     *
     * @var mixed
     */
    var $linkRemoveFillValue = 0;

    /**
     * 指示当保存关联数据时，采用何种方法，默认为 save，可以设置为 create、update 或 replace
     *
     * @var string
     */
    var $saveAssocMethod = 'save';

    /**
     * 主表的表数据入口对象
     *
     * @var FLEA_Db_TableDataGateway
     */
    var $mainTDG;

    /**
     * 关联表的表数据入口对象
     *
     * @var FLEA_Db_TableDataGateway
     */
    var $assocTDG = null;

    /**
     * 必须设置的对象属性
     *
     * @var array
     */
    var $_req = array(
        'name',             // 关联的名字
        'tableClass',       // 关联的表数据入口对象名
        'mappingName',      // 字段映射名
    );

    /**
     * 可选的参数
     *
     * @var array
     */
    var $_optional = array(
        'foreignKey',
        'sort',
        'conditions',
        'fields',
        'enabled',
        'linkRead',
        'linkCreate',
        'linkUpdate',
        'linkRemove',
        'linkRemoveFillValue',
        'recordObjectClass',
        'saveAssocMethod',
    );

    /**
     * 外键字段的完全限定名
     *
     * @var string
     */
    var $qforeignKey;

    /**
     * 数据访问对象
     *
     * @var FLEA_Db_Driver_Prototype
     */
    var $dbo;

    /**
     * 关联表数据入口的对象名
     *
     * @var string
     */
    var $assocTDGObjectId;

    /**
     * 指示关联的表数据入口是否已经初始化
     *
     * @var boolean
     */
    var $init = false;

    /**
     * 构造函数
     *
     * 开发者不应该自行构造 FLEA_Db_TableLink 实例。而是应该通过
     * FLEA_Db_TableLink::createLink() 静态方法来构造实例。
     *
     * @param array $define
     * @param enum $type
     * @param FLEA_Db_TableDataGateway $mainTDG
     *
     * @return FLEA_Db_TableLink
     */
    function FLEA_Db_TableLink($define, $type, & $mainTDG)
    {
        static $defaultDsnId = null;

        // 检查必须的属性是否都已经提供
        foreach ($this->_req as $key) {
            if (!isset($define[$key]) || $define[$key] == '') {
                FLEA::loadClass('FLEA_Db_Exception_MissingLinkOption');
                __THROW(new FLEA_Db_Exception_MissingLinkOption($key));
                return;
            } else {
                $this->{$key} = $define[$key];
            }
        }
        // 设置可选属性
        foreach ($this->_optional as $key) {
            if (isset($define[$key])) {
                $this->{$key} = $define[$key];
            }
        }
        $this->type = $type;
        $this->mainTDG =& $mainTDG;
        $this->dbo =& $this->mainTDG->getDBO();
        $dsnid = $this->dbo->dsn['id'];

        if (is_null($defaultDsnId)) {
            $defaultDSN = FLEA::getAppInf('dbDSN');
            if ($defaultDSN) {
                $defaultDSN = FLEA::parseDSN($defaultDSN);
                $defaultDsnId = $defaultDSN['id'];
            } else {
                $defaultDsnId = -1;
            }
        }
        if ($dsnid == $defaultDsnId) {
            $this->assocTDGObjectId = null;
        } else {
            $this->assocTDGObjectId = "{$this->tableClass}-{$dsnid}";
        }

        $htis->init = false;
    }

    /**
     * 创建 FLEA_Db_TableLink 对象实例
     *
     * @param array $define
     * @param enum $type
     * @param FLEA_Db_TableDataGateway $mainTDG
     *
     * @return FLEA_Db_TableLink
     */
    function & createLink($define, $type, & $mainTDG)
    {
        static $typeMap = array(
            HAS_ONE         => 'FLEA_Db_HasOneLink',
            BELONGS_TO      => 'FLEA_Db_BelongsToLink',
            HAS_MANY        => 'FLEA_Db_HasManyLink',
            MANY_TO_MANY    => 'FLEA_Db_ManyToManyLink',
        );

        // 检查 $type 参数
        if (!isset($typeMap[$type])) {
            FLEA::loadClass('FLEA_Db_Exception_InvalidLinkType');
            __THROW(new FLEA_Db_Exception_InvalidLinkType($type));
            return false;
        }

        // tableClass 属性是必须提供的
        if (!isset($define['tableClass'])) {
            FLEA::loadClass('FLEA_Db_Exception_MissingLinkOption');
            __THROW(new FLEA_Db_Exception_MissingLinkOption('tableClass'));
            return false;
        }
        // 如果没有提供 mappingName 属性，则使用 tableClass 作为 mappingName
        if (!isset($define['mappingName'])) {
            $define['mappingName'] = $define['tableClass'];
        }
        // 如果没有提供 name 属性，则使用 mappingName 属性作为 name
        if (!isset($define['name'])) {
            $define['name'] = $define['mappingName'];
        }

        // 如果是 MANY_TO_MANY 连接，则检查是否提供了 joinTable 属性和
        // assocForeignKey 属性
        if ($type == MANY_TO_MANY) {
            if (!isset($define['joinTable'])) {
                FLEA::loadClass('FLEA_Db_Exception_MissingLinkOption');
                __THROW(new FLEA_Db_Exception_MissingLinkOption('joinTable'));
                return false;
            } else {
                if (!isset($define['joinTable'])) {
                    $define['joinTable'] = $define['joinTable'];
                }
            }
        }

        unset($linkObj);
        $linkObj =& new $typeMap[$type]($define, $type, $mainTDG);
        /* @var $linkObj FLEA_Db_TableLink */
        return $linkObj;
    }

    /**
     * 生成一个 MANY_TO_MANY 关联需要的中间表名称
     *
     * @param string $table1
     * @param string $table2
     *
     * @return string
     */
    function getMiddleTableName($table1, $table2)
    {
        if (strcmp($table1, $table2) < 0) {
            return $this->dbo->dsn['dbTablePrefix'] . "{$table1}_{$table2}";
        } else {
            return $this->dbo->dsn['dbTablePrefix'] . "{$table2}_{$table1}";
        }
    }

    /**
     * 返回用于查询关联表数据的 SQL 语句
     *
     * @param string $sql
     * @param string $in
     *
     * @return string
     */
    function _getFindSQLBase($sql, $in)
    {
        if ($in) {
            $sql .= "\nWHERE {$this->qforeignKey} {$in}";
        }
        if ($this->conditions) {
            if (is_array($this->conditions)) {
                $conditions = FLEA_Db_SqlHelper::parseConditions($this->conditions, $this->assocTDG);
                if (is_array($conditions)) {
                    $conditions = $conditions[0];
                }
            } else {
                $conditions =& $this->conditions;
            }
            if ($conditions) {
                $sql .= " AND {$conditions}";
            }
        }
        if ($this->sort) {
            $sql .= "\nORDER BY {$this->sort}";
        }

        return $sql;
    }

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     * @param mixed $pkv 主表的主键字段值
     *
     * @return boolean
     */
    function saveAssocData(& $row, $pkv)
    {
        FLEA::loadClass('FLEA_Exception_NotImplemented');
        __THROW(new FLEA_Exception_NotImplemented('saveAssocData()', 'FLEA_Db_TableLink'));
        return false;
    }

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     *
     * @return boolean
     */
    function _saveAssocDataBase(& $row)
    {
        switch (strtolower($this->saveAssocMethod)) {
        case 'create':
            return $this->assocTDG->create($row);
        case 'update':
            return $this->assocTDG->update($row);
        case 'replace':
            return $this->assocTDG->replace($row);
        default:
            return $this->assocTDG->save($row);
        }
    }

    /**
     * 初始化关联对象
     */
    function init()
    {
        if ($this->init) { return; }
        if (FLEA::isRegistered($this->assocTDGObjectId)) {
            $this->assocTDG =& FLEA::registry($this->assocTDGObjectId);
        } else {
            if ($this->assocTDGObjectId) {
                FLEA::loadClass($this->tableClass);
                $this->assocTDG =& new $this->tableClass(array('dbo' => & $this->dbo));
                FLEA::register($this->assocTDG, $this->assocTDGObjectId);
            } else {
                $this->assocTDG =& FLEA::getSingleton($this->tableClass);
            }
        }
        $this->init = true;
    }
}

/**
 * FLEA_Db_HasOneLink 封装 has one 关系
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_HasOneLink extends FLEA_Db_TableLink
{
    var $oneToOne = true;

    /**
     * 构造函数
     *
     * @param array $define
     * @param enum $type
     * @param FLEA_Db_TableDataGateway $mainTDG
     *
     * @return FLEA_Db_TableLink
     */
    function FLEA_Db_HasOneLink($define, $type, & $mainTDG)
    {
        parent::FLEA_Db_TableLink($define, $type, $mainTDG);
    }

    /**
     * 返回用于查询关联表数据的SQL语句
     *
     * @param string $in
     *
     * @return string
     */
    function getFindSQL($in)
    {
        if (!$this->init) { $this->init(); }
        $fields = $this->qforeignKey . ' AS ' . $this->mainTDG->pka . ', ' .
                  $this->dbo->qfields($this->fields, $this->assocTDG->fullTableName);
        $sql = <<<EOT
SELECT {$fields}
FROM {$this->assocTDG->qtableName}
EOT;

        return parent::_getFindSQLBase($sql, $in);
    }

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     * @param mixed $pkv 主表的主键字段值
     *
     * @return boolean
     */
    function saveAssocData(& $row, $pkv)
    {
        if (empty($row)) { return true; }
        if (!$this->init) { $this->init(); }
        $row[$this->foreignKey] = $pkv;
        return $this->_saveAssocDataBase($row);
    }

    /**
     * 删除关联的数据
     *
     * @param mixed $qpkv
     *
     * @return boolean
     */
    function deleteByForeignKey($qpkv)
    {
        if (!$this->init) { $this->init(); }
        $conditions = "{$this->qforeignKey} = {$qpkv}";
        if ($this->linkRemove) {
            return $this->assocTDG->removeByConditions($conditions);
        } else {
            return $this->assocTDG->updateField($conditions, $this->foreignKey,
                                                $this->linkRemoveFillValue);
        }
    }

    /**
     * 完全初始化关联对象
     */
    function init()
    {
        parent::init();
        if ($this->foreignKey == null) {
            $this->foreignKey = $this->mainTDG->primaryKey;
        }
        $this->qforeignKey = $this->dbo->qfield($this->foreignKey,
                                                  $this->assocTDG->fullTableName);
    }
}

/**
 * FLEA_Db_BelongsToLink 封装 belongs to 关系
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_BelongsToLink extends FLEA_Db_TableLink
{
    var $oneToOne = true;

    /**
     * 构造函数
     *
     * @param array $define
     * @param enum $type
     * @param FLEA_Db_TableDataGateway $mainTDG
     *
     * @return FLEA_Db_TableLink
     */
    function FLEA_Db_BelongsToLink($define, $type, & $mainTDG)
    {
        parent::FLEA_Db_TableLink($define, $type, $mainTDG);
    }

    /**
     * 返回用于查询关联表数据的SQL语句
     *
     * @param string $in
     *
     * @return string
     */
    function getFindSQL($in)
    {
        if (!$this->init) { $this->init(); }
        $fields = $this->mainTDG->qpk . ' AS ' . $this->mainTDG->pka . ', ' .
                  $this->dbo->qfields($this->fields, $this->assocTDG->fullTableName);

        $sql = <<<EOT
SELECT {$fields}
FROM {$this->assocTDG->qtableName}
LEFT JOIN {$this->mainTDG->qtableName}
    ON {$this->mainTDG->qpk} {$in}
WHERE {$this->qforeignKey} = {$this->assocTDG->qpk}
EOT;

        $in = '';
        return parent::_getFindSQLBase($sql, $in);
    }

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     * @param mixed $pkv 主表的主键字段值
     *
     * @return boolean
     */
    function saveAssocData(& $row, $pkv)
    {
        if (empty($row)) { return true; }
        if (!$this->init) { $this->init(); }
        return $this->_saveAssocDataBase($row);
    }

    /**
     * 完全初始化关联对象
     */
    function init()
    {
        parent::init();
        if ($this->foreignKey == null) {
            $this->foreignKey = $this->assocTDG->primaryKey;
        }
        $this->qforeignKey = $this->dbo->qfield($this->foreignKey,
                                                  $this->mainTDG->fullTableName);
    }
}

/**
 * FLEA_Db_HasManyLink 封装 has many 关系
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_HasManyLink extends FLEA_Db_HasOneLink
{
    var $oneToOne = false;

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     * @param mixed $pkv 主表的主键字段值
     *
     * @return boolean
     */
    function saveAssocData(& $row, $pkv)
    {
        if (empty($row)) { return true; }
        if (!$this->init) { $this->init(); }

        foreach ($row as $arow) {
            if (!is_array($arow)) { continue; }
            $arow[$this->foreignKey] = $pkv;
            if (!$this->_saveAssocDataBase($arow)) {
                return false;
            }
        }
        return true;
    }
}

/**
 * FLEA_Db_ManyToManyLink 封装 many to many 关系
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_ManyToManyLink extends FLEA_Db_TableLink
{
    var $oneToOne = false;

    /**
     * 中间表的名字
     *
     * @var string
     */
    var $joinTable = null;

    /**
     * 中间表的完全限定名
     *
     * @var string
     */
    var $qjoinTable;

    /**
     * 中间表中保存关联表主键值的字段
     *
     * @var string
     */
    var $assocForeignKey = null;

    /**
     * 中间表中保存关联表主键值的字段的完全限定名
     *
     * @var string
     */
    var $qassocForeignKey;

    /**
     * 构造函数
     *
     * @param array $define
     * @param enum $type
     * @param FLEA_Db_TableDataGateway $mainTDG
     *
     * @return FLEA_Db_TableLink
     */
    function FLEA_Db_ManyToManyLink($define, $type, & $mainTDG)
    {
        $this->_optional[] = 'joinTable';
        $this->_optional[] = 'assocForeignKey';
        parent::FLEA_Db_TableLink($define, $type, $mainTDG);
    }

    /**
     * 返回用于查询关联表数据的SQL语句
     *
     * @param string $in
     *
     * @return string
     */
    function getFindSQL($in)
    {
        if (!$this->init) { $this->init(); }
        $fields = $this->qforeignKey . ' AS ' . $this->mainTDG->pka . ', ' .
                  $this->dbo->qfields($this->fields, $this->assocTDG->fullTableName);

        $sql = <<<EOT
SELECT {$fields}
FROM {$this->qjoinTable}
INNER JOIN {$this->assocTDG->qtableName}
    ON {$this->assocTDG->qpk} = {$this->qassocForeignKey}
EOT;

        return parent::_getFindSQLBase($sql, $in);
    }

    /**
     * 创建或更新主表记录时，保存关联的数据
     *
     * @param array $row 要保存的关联数据
     * @param mixed $pkv 主表的主键字段值
     *
     * @return boolean
     */
    function saveAssocData(& $row, $pkv)
    {
        if (!$this->init) { $this->init(); }
        $apkvs = array();
        foreach ($row as $arow) {
            if (!is_array($arow)) {
                $apkvs[] = $arow;
                continue;
            }

            if (!isset($arow[$this->assocTDG->primaryKey])) {
                // 如果关联记录尚未保存到数据库，则创建一条新的关联记录
                $newrowid = $this->assocTDG->create($arow);
                if ($newrowid == false) {
                    return false;
                }
                $apkvs[] = $newrowid;
            } else {
                $apkvs[] = $arow[$this->assocTDG->primaryKey];
            }
        }

        // 首先取出现有的关联信息
        $qpkv = $this->dbo->qstr($pkv);
        $sql = <<<EOT
SELECT {$this->qassocForeignKey}
FROM {$this->qjoinTable}
WHERE {$this->qforeignKey} = {$qpkv}
EOT;

        $existsMiddle = (array)$this->dbo->getCol($sql);

        // 然后确定要添加的关联信息
        $insertAssoc = array_diff($apkvs, $existsMiddle);
        $removeAssoc = array_diff($existsMiddle, $apkvs);
        $sql = <<<EOT
INSERT INTO {$this->qjoinTable}
    ({$this->qforeignKey}, {$this->qassocForeignKey})
VALUES
    ({$qpkv},
EOT;

        foreach ($insertAssoc as $assocId) {
            if (!$this->dbo->execute($sql . $this->dbo->qstr($assocId) . ')')) {
                return false;
            }
        }

        // 最后删除不再需要的关联信息
        $sql = <<<EOT
DELETE FROM {$this->qjoinTable}
WHERE {$this->qforeignKey} = {$qpkv} AND {$this->qassocForeignKey} =
EOT;
        foreach ($removeAssoc as $assocId) {
            if (!$this->dbo->execute($sql . $this->dbo->qstr($assocId))) {
                return false;
            }
        }

        return true;
    }

    /**
     * 根据主表的外键字段值，删除中间表的数据
     *
     * @param mixed $qpkv
     *
     * @return boolean
     */
    function deleteMiddleTableDataByMainForeignKey($qpkv)
    {
        if (!$this->init) { $this->init(); }
        $sql = <<<EOT
DELETE FROM {$this->qjoinTable}
WHERE {$this->qforeignKey} = {$qpkv}
EOT;
        return $this->dbo->execute($sql);
    }

    /**
     * 根据关联表的外键字段值，删除中间表的数据
     *
     * @param mixed $pkv
     *
     * @return boolean
     */
    function deleteMiddleTableDataByAssocForeignKey($pkv)
    {
        if (!$this->init) { $this->init(); }
        $qpkv = $this->dbo->qstr($pkv);
        $sql = <<<EOT
DELETE FROM {$this->qjoinTable}
WHERE {$this->qassocForeignKey} = {$qpkv}
EOT;
        return $this->dbo->execute($sql);
    }

    /**
     * 完全初始化关联对象
     */
    function init()
    {
        parent::init();
        if ($this->joinTable == null) {
            $this->joinTable = $this->getMiddleTableName($this->mainTDG->tableName,
                                                         $this->assocTableName);
        }
        if ($this->foreignKey == null) {
            $this->foreignKey = $this->mainTDG->primaryKey;
        }
        $this->joinTable = $this->dbo->dsn['dbTablePrefix'] . $this->joinTable;
        $this->qjoinTable = $this->dbo->qtable($this->joinTable);
        $this->qforeignKey = $this->dbo->qfield($this->foreignKey,
                                                  $this->joinTable);
        if ($this->assocForeignKey == null) {
            $this->assocForeignKey = $this->assocTDG->primaryKey;
        }
        $this->qassocForeignKey = $this->dbo->qfield($this->assocForeignKey,
                                                       $this->joinTable);
    }
}

/**
 * FLEA_Db_SqlHelper 类提供了各种生成 SQL 语句的辅助方法
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_SqlHelper
{
    /**
     * 分析查询条件
     *
     * @param mixed $conditions
     * @param FLEA_Db_TableDataGateway $table
     *
     * @return array
     */
    function parseConditions($conditions, & $table)
    {
        // 对于 NULL，直接返回 NULL
        if (is_null($conditions)) { return null; }

        // 如果是数字，则假定为主键字段值
        if (is_numeric($conditions)) {
            return "{$table->qpk} = {$conditions}";
        }

        // 如果是字符串，则假定为自定义条件
        if (is_string($conditions)) {
            return $conditions;
        }

        // 如果不是数组，说明提供的查询条件有误
        if (!is_array($conditions)) {
            return null;
        }

        $where = '';
        $linksWhere = array();
        $expr = '';

        foreach ($conditions as $offset => $cond) {
            $expr = 'AND';
            /**
             * 不过何种条件形式，一律转换为 (字段名, 值, 操作, 连接运算符, 值是否是SQL命令) 的形式
             */
            if (is_string($offset)) {
                if (!is_array($cond)) {
                    // 字段名 => 值
                    $cond = array($offset, $cond);
                } else {
                    // 字段名 => 数组
                    array_unshift($cond, $offset);
                }
            } elseif (is_int($offset)) {
                if (!is_array($cond)) {
                    // 值
                    $cond = array('', $cond, '', $expr, true);
                }
            } else {
                continue;
            }

            if (!isset($cond[0])) { continue; }
            if (!isset($cond[2])) { $cond[2] = '='; }
            if (!isset($cond[3])) { $cond[3] = $expr; }
            if (!isset($cond[4])) { $cond[4] = false; }

            list($field, $value, $op, $expr, $isCommand) = $cond;

            $str = '';
            do {
                if (strpos($field, '.') !== false) {
                    list($scheme, $field) = explode('.', $field);
                    $linkname = strtoupper($scheme);
                    if (isset($table->links[$linkname])) {
                        $linksWhere[$linkname][] = array($field, $value, $op, $expr, $isCommand);
                        break;
                    } else {
                        $field = "{$scheme}.{$field}";
                    }
                }

                if (!$isCommand) {
                    $field = $table->qfield($field);
                    $value = $table->dbo->qstr($value);
                    $str = "{$field} {$op} {$value} {$expr} ";
                } else {
                    $str = "{$value} {$expr} ";
                }
            } while (false);

            $where .= $str;
        }

        $where = substr($where, 0, - (strlen($expr) + 2));
        if (empty($linksWhere)) {
            return $where;
        } else {
            return array($where, $linksWhere);
        }
    }

    /**
     * 格式化输出 SQL 日志
     *
     * @param array $log
     */
    function dumpLog(& $log)
    {
        foreach ($log as $ix => $sql) {
            dump($sql, 'SQL ' . ($ix + 1));
        }
    }
}
